fix: pre-empt agent library stream error throws with sanitized finalize code#347
fix: pre-empt agent library stream error throws with sanitized finalize code#347JeffOtano wants to merge 1 commit into
Conversation
…ze code @convex-dev/agent@0.6.1 processes saveStreamDeltas events in its internal finalizeMessage mutation using the AI SDK stream processor. When a provider error event (e.g. Gemini high-demand 503) arrives in those deltas, the mutation throws the raw provider error text. Because this mutation runs in a Convex V8 isolate independently from the calling action, Convex reports the throw to Sentry before any action-level catch handler can intercept it, creating noise for every transient provider overload. Fix: add an onError callback to thread.streamText() in attemptStream that pre-emptively calls safeFinalizePending with the sanitized error code before result.text rejects. If our finalization runs first, the agent library finds the message already in "failed" state and skips delta processing, preventing the raw provider error from reaching the V8 isolate throw path. Also extract the error-reporting cluster to resilienceReporting.ts to keep resilience.ts under the 400-line hard cap. https://claude.ai/code/session_01CJ3Ax6zGpqhHdFU36dwaLx
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Closing as code-owner triage. After #348 landed, this draft conflicts in convex/ai/resilience.ts and convex/ai/resilienceStreamFailure.test.ts. It also changes stream finalization timing, so it needs a clean follow-up from current main with the Gemini thinking fix preserved before it can be considered safe. |
Summary
@convex-dev/agent@0.6.1stores stream events viasaveStreamDeltasand processes them in its internalfinalizeMessageV8 mutation using the AI SDK'sprocess-ui-message-stream.ts. When a provider error event arrives in those stored deltas (e.g. a Gemini "high demand" 503), the mutation throws the raw provider error text. Because this mutation runs in a Convex V8 isolate independently from the calling action, Convex's built-in Sentry integration reports the throw before any action-level.catch()can intercept it — creating noise for every transient provider overload.onErrorcallback tothread.streamText()inattemptStreamthat pre-emptively callssafeFinalizePendingwith the sanitized error code (e.g."provider_overload") beforeresult.textrejects. If our finalization runs first, the agent library'sfinalizeMessagefinds the message already in"failed"state and skips delta processing — preventing the raw error from reaching the V8 throw path.finalizePendingMessages,reportError,tryReportByok, and their safe wrappers +getFinalizeCodeForError) was extracted to a newresilienceReporting.tsto keepresilience.tsunder the 400-line hard cap.Why a proper fix, not a suppression
Prior attempts added the Gemini "high demand" substring to
sentryBeforeSend.ts(Next.js Sentry filter), but that only filters the client-side throw. The V8 mutation throw goes through Convex's separate Sentry integration, which bypassesbeforeSendentirely. The only correct fix is to prevent the throw from happening in the first place.Test plan
convex/ai/resilienceStreamFailure.test.ts— 4 new tests added to"onError pre-emptive finalization"describe block:onErrorfires with Gemini high-demand →finalizeMessagecalled with"provider_overload"(not raw text)onErrorfires with Claude overload → same"provider_overload"codeonErrorfired) → no spuriousfinalizeMessagecalls./node_modules/.bin/vitest run --project backend)tsc --noEmit)https://claude.ai/code/session_01CJ3Ax6zGpqhHdFU36dwaLx
Generated by Claude Code